using System.Collections;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.Events;

/// <summary>
///     An IEventDispatcher that queues events.
/// </summary>
/// <remarks>
///     <para>Can raise, or ignore, cancelable events, depending on option.</para>
///     <para>
///         Implementations must override ScopeExitCompleted to define what
///         to do with the events when the scope exits and has been completed.
///     </para>
///     <para>If the scope exits without being completed, events are ignored.</para>
/// </remarks>
public abstract class QueuingEventDispatcherBase : IEventDispatcher
{
    private readonly bool _raiseCancelable;

    // events will be enlisted in the order they are raised
    private List<IEventDefinition>? _events;

    protected QueuingEventDispatcherBase(bool raiseCancelable) => _raiseCancelable = raiseCancelable;

    private List<IEventDefinition> Events => _events ??= new List<IEventDefinition>();

    public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null)
    {
        if (eventHandler == null)
        {
            return args.Cancel;
        }

        if (_raiseCancelable == false)
        {
            return args.Cancel;
        }

        eventHandler(sender, args);
        return args.Cancel;
    }

    public bool DispatchCancelable<TArgs>(EventHandler<TArgs> eventHandler, object sender, TArgs args, string? eventName = null)
        where TArgs : CancellableEventArgs
    {
        if (eventHandler == null)
        {
            return args.Cancel;
        }

        if (_raiseCancelable == false)
        {
            return args.Cancel;
        }

        eventHandler(sender, args);
        return args.Cancel;
    }

    public bool DispatchCancelable<TSender, TArgs>(TypedEventHandler<TSender, TArgs> eventHandler, TSender sender, TArgs args, string? eventName = null)
        where TArgs : CancellableEventArgs
    {
        if (eventHandler == null)
        {
            return args.Cancel;
        }

        if (_raiseCancelable == false)
        {
            return args.Cancel;
        }

        eventHandler(sender, args);
        return args.Cancel;
    }

    public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null)
    {
        if (eventHandler == null)
        {
            return;
        }

        Events.Add(new EventDefinition(eventHandler, sender, args, eventName));
    }

    public void Dispatch<TArgs>(EventHandler<TArgs> eventHandler, object sender, TArgs args, string? eventName = null)
    {
        if (eventHandler == null)
        {
            return;
        }

        Events.Add(new EventDefinition<TArgs>(eventHandler, sender, args, eventName));
    }

    public void Dispatch<TSender, TArgs>(TypedEventHandler<TSender, TArgs> eventHandler, TSender sender, TArgs args, string? eventName = null)
    {
        if (eventHandler == null)
        {
            return;
        }

        Events.Add(new EventDefinition<TSender, TArgs>(eventHandler, sender, args, eventName));
    }

    public IEnumerable<IEventDefinition> GetEvents(EventDefinitionFilter filter)
    {
        if (_events == null)
        {
            return Enumerable.Empty<IEventDefinition>();
        }

        IReadOnlyList<IEventDefinition> events;
        switch (filter)
        {
            case EventDefinitionFilter.All:
                events = _events;
                break;
            case EventDefinitionFilter.FirstIn:
                var l1 = new OrderedHashSet<IEventDefinition>();
                foreach (IEventDefinition e in _events)
                {
                    l1.Add(e);
                }

                events = l1;
                break;
            case EventDefinitionFilter.LastIn:
                var l2 = new OrderedHashSet<IEventDefinition>(false);
                foreach (IEventDefinition e in _events)
                {
                    l2.Add(e);
                }

                events = l2;
                break;
            default:
                throw new ArgumentOutOfRangeException("filter", filter, null);
        }

        return FilterSupersededAndUpdateToLatestEntity(events);
    }

    public void ScopeExit(bool completed)
    {
        if (_events == null)
        {
            return;
        }

        if (completed)
        {
            ScopeExitCompleted();
        }

        _events.Clear();
    }

    // this is way too convoluted, the supersede attribute is used only on DeleteEventargs to specify
    // that it supersedes save, publish, move and copy - BUT - publish event args is also used for
    // unpublishing and should NOT be superseded - so really it should not be managed at event args
    // level but at event level
    //
    // what we want is:
    // if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should
    // not trigger for the entity - and even though, does it make any sense? making a copy of an entity
    // should ... trigger?
    //
    // not going to refactor it all - we probably want to *always* trigger event but tell people that
    // due to scopes, they should not expected eg a saved entity to still be around - however, now,
    // going to write a ugly condition to deal with U4-10764

    // iterates over the events (latest first) and filter out any events or entities in event args that are included
    // in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want
    // to raise the Saved event (well actually we just don't want to include it in the args for that saved event)
    internal static IEnumerable<IEventDefinition> FilterSupersededAndUpdateToLatestEntity(
        IReadOnlyList<IEventDefinition> events)
    {
        // keeps the 'latest' entity and associated event data
        var entities = new List<Tuple<IEntity, EventDefinitionInfos>>();

        // collects the event definitions
        // collects the arguments in result, that require their entities to be updated
        var result = new List<IEventDefinition>();
        var resultArgs = new List<CancellableObjectEventArgs>();

        // eagerly fetch superseded arg types for each arg type
        var argTypeSuperceeding = events.Select(x => x.Args.GetType())
            .Distinct()
            .ToDictionary(
                x => x,
                x => x.GetCustomAttributes<SupersedeEventAttribute>(false).Select(y => y.SupersededEventArgsType)
                    .ToArray());

        // iterate over all events and filter
        //
        // process the list in reverse, because events are added in the order they are raised and we want to keep
        // the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity
        // is Deleted after being Saved, we want to filter out the Saved event
        for (var index = events.Count - 1; index >= 0; index--)
        {
            IEventDefinition def = events[index];

            var infos = new EventDefinitionInfos
            {
                EventDefinition = def,
                SupersedeTypes = argTypeSuperceeding[def.Args.GetType()],
            };

            var args = def.Args as CancellableObjectEventArgs;
            if (args == null)
            {
                // not a cancellable event arg, include event definition in result
                result.Add(def);
            }
            else
            {
                // event object can either be a single object or an enumerable of objects
                // try to get as an enumerable, get null if it's not
                IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
                if (eventObjects == null)
                {
                    // single object, cast as an IEntity
                    // if cannot cast, cannot filter, nothing - just include event definition in result
                    if (args.EventObject is not IEntity eventEntity)
                    {
                        result.Add(def);
                        continue;
                    }

                    // look for this entity in superseding event args
                    // found = must be removed (ie not added), else track
                    if (IsSuperceeded(eventEntity, infos, entities) == false)
                    {
                        // track
                        entities.Add(Tuple.Create(eventEntity, infos));

                        // track result arguments
                        // include event definition in result
                        resultArgs.Add(args);
                        result.Add(def);
                    }
                }
                else
                {
                    // enumerable of objects
                    var toRemove = new List<IEntity>();
                    foreach (var eventObject in eventObjects)
                    {
                        // extract the event object, cast as an IEntity
                        // if cannot cast, cannot filter, nothing to do - just leave it in the list & continue
                        if (eventObject is not IEntity eventEntity)
                        {
                            continue;
                        }

                        // look for this entity in superseding event args
                        // found = must be removed, else track
                        if (IsSuperceeded(eventEntity, infos, entities))
                        {
                            toRemove.Add(eventEntity);
                        }
                        else
                        {
                            entities.Add(Tuple.Create(eventEntity, infos));
                        }
                    }

                    // remove superseded entities
                    foreach (IEntity entity in toRemove)
                    {
                        eventObjects.Remove(entity);
                    }

                    // if there are still entities in the list, keep the event definition
                    if (eventObjects.Count > 0)
                    {
                        if (toRemove.Count > 0)
                        {
                            // re-assign if changed
                            args.EventObject = eventObjects;
                        }

                        // track result arguments
                        // include event definition in result
                        resultArgs.Add(args);
                        result.Add(def);
                    }
                }
            }
        }

        // go over all args in result, and update them with the latest instanceof each entity
        UpdateToLatestEntities(entities, resultArgs);

        // reverse, since we processed the list in reverse
        result.Reverse();

        return result;
    }

    protected abstract void ScopeExitCompleted();

    // edits event args to use the latest instance of each entity
    private static void UpdateToLatestEntities(
        IEnumerable<Tuple<IEntity, EventDefinitionInfos>> entities,
        IEnumerable<CancellableObjectEventArgs> args)
    {
        // get the latest entities
        // ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates)
        var latestEntities = new OrderedHashSet<IEntity>(true);
        foreach (Tuple<IEntity, EventDefinitionInfos> entity in entities.OrderByDescending(entity =>
                     entity.Item1.UpdateDate))
        {
            latestEntities.Add(entity.Item1);
        }

        foreach (CancellableObjectEventArgs arg in args)
        {
            // event object can either be a single object or an enumerable of objects
            // try to get as an enumerable, get null if it's not
            IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject);
            if (eventObjects == null)
            {
                // single object
                // look for a more recent entity for that object, and replace if any
                // works by "equalling" entities ie the more recent one "equals" this one (though different object)
                IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject));
                if (foundEntity != null)
                {
                    arg.EventObject = foundEntity;
                }
            }
            else
            {
                // enumerable of objects
                // same as above but for each object
                var updated = false;
                for (var i = 0; i < eventObjects.Count; i++)
                {
                    IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i]));
                    if (foundEntity == null)
                    {
                        continue;
                    }

                    eventObjects[i] = foundEntity;
                    updated = true;
                }

                if (updated)
                {
                    arg.EventObject = eventObjects;
                }
            }
        }
    }

    // determines if a given entity, appearing in a given event definition, should be filtered out,
    // considering the entities that have already been visited - an entity is filtered out if it
    // appears in another even definition, which supersedes this event definition.
    private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List<Tuple<IEntity, EventDefinitionInfos>> entities)
    {
        // var argType = meta.EventArgsType;
        Type? argType = infos.EventDefinition?.Args.GetType();

        // look for other instances of the same entity, coming from an event args that supersedes other event args,
        // ie is marked with the attribute, and is not this event args (cannot supersede itself)
        Tuple<IEntity, EventDefinitionInfos>[] superceeding = entities
            .Where(x => x.Item2.SupersedeTypes?.Length > 0 // has the attribute
                        && x.Item2.EventDefinition?.Args.GetType() != argType // is not the same
                        && Equals(x.Item1, entity)) // same entity
            .ToArray();

        // first time we see this entity = not filtered
        if (superceeding.Length == 0)
        {
            return false;
        }

        // delete event args does NOT supersedes 'unpublished' event
        if ((argType?.IsGenericType ?? false) && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) &&
            infos.EventDefinition?.EventName == "Unpublished")
        {
            return false;
        }

        // found occurrences, need to determine if this event args is superseded
        if (argType?.IsGenericType ?? false)
        {
            // generic, must compare type arguments
            Tuple<IEntity, EventDefinitionInfos>? supercededBy = superceeding.FirstOrDefault(x =>
                x.Item2.SupersedeTypes?.Any(y =>

                    // superseding a generic type which has the same generic type definition
                    // (but ... no matter the generic type parameters? could be different?)
                    (y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition())

                    // or superceeding a non-generic type which is ... (but... how is this ever possible? argType *is* generic?
                    || (y.IsGenericTypeDefinition == false && y == argType)) ?? false);
            return supercededBy != null;
        }
        else
        {
            // non-generic, can compare types 1:1
            Tuple<IEntity, EventDefinitionInfos>? supercededBy = superceeding.FirstOrDefault(x =>
                x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false);
            return supercededBy != null;
        }
    }

    private sealed class EventDefinitionInfos
    {
        public IEventDefinition? EventDefinition { get; set; }

        public Type[]? SupersedeTypes { get; set; }
    }
}
